/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.support.v4.content;

import android.content.Context;
import android.os.Handler;
import android.os.SystemClock;
import android.support.v4.util.TimeUtils;
import android.util.Log;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.concurrent.CountDownLatch;

/**
 * Static library support version of the framework's
 * {@link android.content.AsyncTaskLoader}. Used to write apps that run on
 * platforms prior to Android 3.0. When running on Android 3.0 or above, this
 * implementation is still used; it does not try to switch to the framework's
 * implementation. See the framework SDK documentation for a class overview.
 */
public abstract class AsyncTaskLoader<D> extends Loader<D> {
	static final String TAG = "AsyncTaskLoader";
	static final boolean DEBUG = false;

	final class LoadTask extends ModernAsyncTask<Void, Void, D> implements Runnable {

		D result;
		boolean waiting;

		private CountDownLatch done = new CountDownLatch(1);

		/* Runs on a worker thread */
		@Override
		protected D doInBackground(Void... params) {
			if (DEBUG)
				Log.v(TAG, this + " >>> doInBackground");
			result = AsyncTaskLoader.this.onLoadInBackground();
			if (DEBUG)
				Log.v(TAG, this + "  <<< doInBackground");
			return result;
		}

		/* Runs on the UI thread */
		@Override
		protected void onPostExecute(D data) {
			if (DEBUG)
				Log.v(TAG, this + " onPostExecute");
			try {
				AsyncTaskLoader.this.dispatchOnLoadComplete(this, data);
			} finally {
				done.countDown();
			}
		}

		@Override
		protected void onCancelled() {
			if (DEBUG)
				Log.v(TAG, this + " onCancelled");
			try {
				AsyncTaskLoader.this.dispatchOnCancelled(this, result);
			} finally {
				done.countDown();
			}
		}

		@Override
		public void run() {
			waiting = false;
			AsyncTaskLoader.this.executePendingTask();
		}
	}

	volatile LoadTask mTask;
	volatile LoadTask mCancellingTask;

	long mUpdateThrottle;
	long mLastLoadCompleteTime = -10000;
	Handler mHandler;

	public AsyncTaskLoader(Context context) {
		super(context);
	}

	/**
	 * Set amount to throttle updates by. This is the minimum time from when the
	 * last {@link #onLoadInBackground()} call has completed until a new load is
	 * scheduled.
	 * 
	 * @param delayMS
	 *            Amount of delay, in milliseconds.
	 */
	public void setUpdateThrottle(long delayMS) {
		mUpdateThrottle = delayMS;
		if (delayMS != 0) {
			mHandler = new Handler();
		}
	}

	@Override
	protected void onForceLoad() {
		super.onForceLoad();
		cancelLoad();
		mTask = new LoadTask();
		if (DEBUG)
			Log.v(TAG, "Preparing load: mTask=" + mTask);
		executePendingTask();
	}

	/**
	 * Attempt to cancel the current load task. See
	 * {@link android.os.AsyncTask#cancel(boolean)} for more info. Must be
	 * called on the main thread of the process.
	 * 
	 * <p>
	 * Cancelling is not an immediate operation, since the load is performed in
	 * a background thread. If there is currently a load in progress, this
	 * method requests that the load be cancelled, and notes this is the case;
	 * once the background thread has completed its work its remaining state
	 * will be cleared. If another load request comes in during this time, it
	 * will be held until the cancelled load is complete.
	 * 
	 * @return Returns <tt>false</tt> if the task could not be cancelled,
	 *         typically because it has already completed normally, or because
	 *         {@link #startLoading()} hasn't been called; returns <tt>true</tt>
	 *         otherwise.
	 */
	public boolean cancelLoad() {
		if (DEBUG)
			Log.v(TAG, "cancelLoad: mTask=" + mTask);
		if (mTask != null) {
			if (mCancellingTask != null) {
				// There was a pending task already waiting for a previous
				// one being canceled; just drop it.
				if (DEBUG)
					Log.v(TAG, "cancelLoad: still waiting for cancelled task; dropping next");
				if (mTask.waiting) {
					mTask.waiting = false;
					mHandler.removeCallbacks(mTask);
				}
				mTask = null;
				return false;
			} else if (mTask.waiting) {
				// There is a task, but it is waiting for the time it should
				// execute. We can just toss it.
				if (DEBUG)
					Log.v(TAG, "cancelLoad: task is waiting, dropping it");
				mTask.waiting = false;
				mHandler.removeCallbacks(mTask);
				mTask = null;
				return false;
			} else {
				boolean cancelled = mTask.cancel(false);
				if (DEBUG)
					Log.v(TAG, "cancelLoad: cancelled=" + cancelled);
				if (cancelled) {
					mCancellingTask = mTask;
				}
				mTask = null;
				return cancelled;
			}
		}
		return false;
	}

	/**
	 * Called if the task was canceled before it was completed. Gives the class
	 * a chance to properly dispose of the result.
	 */
	public void onCanceled(D data) {
	}

	void executePendingTask() {
		if (mCancellingTask == null && mTask != null) {
			if (mTask.waiting) {
				mTask.waiting = false;
				mHandler.removeCallbacks(mTask);
			}
			if (mUpdateThrottle > 0) {
				long now = SystemClock.uptimeMillis();
				if (now < (mLastLoadCompleteTime + mUpdateThrottle)) {
					// Not yet time to do another load.
					if (DEBUG)
						Log.v(TAG, "Waiting until " + (mLastLoadCompleteTime + mUpdateThrottle) + " to execute: " + mTask);
					mTask.waiting = true;
					mHandler.postAtTime(mTask, mLastLoadCompleteTime + mUpdateThrottle);
					return;
				}
			}
			if (DEBUG)
				Log.v(TAG, "Executing: " + mTask);
			mTask.executeOnExecutor(ModernAsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
		}
	}

	void dispatchOnCancelled(LoadTask task, D data) {
		onCanceled(data);
		if (mCancellingTask == task) {
			if (DEBUG)
				Log.v(TAG, "Cancelled task is now canceled!");
			mLastLoadCompleteTime = SystemClock.uptimeMillis();
			mCancellingTask = null;
			executePendingTask();
		}
	}

	void dispatchOnLoadComplete(LoadTask task, D data) {
		if (mTask != task) {
			if (DEBUG)
				Log.v(TAG, "Load complete of old task, trying to cancel");
			dispatchOnCancelled(task, data);
		} else {
			if (isAbandoned()) {
				// This cursor has been abandoned; just cancel the new data.
				onCanceled(data);
			} else {
				mLastLoadCompleteTime = SystemClock.uptimeMillis();
				mTask = null;
				if (DEBUG)
					Log.v(TAG, "Delivering result");
				deliverResult(data);
			}
		}
	}

	/**
     */
	public abstract D loadInBackground();

	/**
	 * Called on a worker thread to perform the actual load. Implementations
	 * should not deliver the result directly, but should return them from this
	 * method, which will eventually end up calling {@link #deliverResult} on
	 * the UI thread. If implementations need to process the results on the UI
	 * thread they may override {@link #deliverResult} and do so there.
	 * 
	 * @return Implementations must return the result of their load operation.
	 */
	protected D onLoadInBackground() {
		return loadInBackground();
	}

	/**
	 * Locks the current thread until the loader completes the current load
	 * operation. Returns immediately if there is no load operation running.
	 * Should not be called from the UI thread: calling it from the UI thread
	 * would cause a deadlock.
	 * <p>
	 * Use for testing only. <b>Never</b> call this from a UI thread.
	 * 
	 * @hide
	 */
	public void waitForLoader() {
		LoadTask task = mTask;
		if (task != null) {
			try {
				task.done.await();
			} catch (InterruptedException e) {
				// Ignore
			}
		}
	}

	@Override
	public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
		super.dump(prefix, fd, writer, args);
		if (mTask != null) {
			writer.print(prefix);
			writer.print("mTask=");
			writer.print(mTask);
			writer.print(" waiting=");
			writer.println(mTask.waiting);
		}
		if (mCancellingTask != null) {
			writer.print(prefix);
			writer.print("mCancellingTask=");
			writer.print(mCancellingTask);
			writer.print(" waiting=");
			writer.println(mCancellingTask.waiting);
		}
		if (mUpdateThrottle != 0) {
			writer.print(prefix);
			writer.print("mUpdateThrottle=");
			TimeUtils.formatDuration(mUpdateThrottle, writer);
			writer.print(" mLastLoadCompleteTime=");
			TimeUtils.formatDuration(mLastLoadCompleteTime, SystemClock.uptimeMillis(), writer);
			writer.println();
		}
	}
}
